﻿#region License

// Dimebrain TweetSharp
// (www.dimebrain.com)
// 
// The MIT License
// 
// Copyright (c) 2009 Dimebrain
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// 

#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using Dimebrain.TweetSharp.Core;
using Dimebrain.TweetSharp.Core.Caching;
using Dimebrain.TweetSharp.Core.Exceptions;
using Dimebrain.TweetSharp.Core.Extensions;
using Dimebrain.TweetSharp.Core.OAuth;
using Dimebrain.TweetSharp.Core.Tasks;
using Dimebrain.TweetSharp.Core.Web;
using Dimebrain.TweetSharp.Core.Web.OAuth;
using Dimebrain.TweetSharp.Core.Web.Query;
using Dimebrain.TweetSharp.Core.Web.Query.Basic;
using Dimebrain.TweetSharp.Core.Web.Query.OAuth;
using Dimebrain.TweetSharp.Extensions;
using Dimebrain.TweetSharp.Fluent.Services;
using Dimebrain.TweetSharp.Fluent.Twitter.Authentication;
using Dimebrain.TweetSharp.Fluent.Twitter.Services;
using Dimebrain.TweetSharp.Model;
#if !SILVERLIGHT
using System.Drawing;
using System.Drawing.Imaging;
#endif

namespace Dimebrain.TweetSharp.Fluent
{
#if !SILVERLIGHT
    /// <summary>
    /// This is the main fluent class for building expressions
    /// bound for the Twitter API.
    /// </summary>
    [Serializable]
#endif
    public sealed partial class FluentTwitter : IFluentTwitter
    {
        private static TwitterClientInfo _clientInfo;

        private const string TWITTER_CLIENT_DEFAULT_NAME = "tweetsharp";
        private const string TWITTER_CLIENT_DEFAULT_URL = "http://tweetsharp.com";
        private const string TWITTER_CLIENT_DEFAULT_VERSION = "1.0.0.0";
        private const int TWITTER_MAX_UPDATE_LENGTH = 140;
        
        private FluentTwitter(TwitterClientInfo clientInfo)
        {
#if !SILVERLIGHT && !Smartphone
            var twitter = new Regex(@"http://twitter\.com/.*");
            var permission = new WebPermission(NetworkAccess.Connect, twitter);
            permission.Demand();
#endif
            ClientInfo = clientInfo;

            Profile = new FluentTwitterProfile();
            Authentication = new FluentTwitterAuthentication(this);
            SecondaryAuthentication = new FluentTwitterAuthentication(this);
            Configuration = new FluentTwitterConfiguration(this);
            SearchParameters = new FluentTwitterSearchParameters();
            Parameters = new FluentTwitterParameters();

#if !SILVERLIGHT
            // http://groups.google.com/group/twitter-development-talk/browse_thread/thread/7c67ff1a2407dee7
            ServicePointManager.Expect100Continue = false;
#else
            // todo modify Expect100Continue behavior directly
#endif 
        }

        /// <summary>
        /// Gets the authentication pair used to authenticate to twitter.
        /// </summary>
        /// <value>The authentication pair, typically a username and password or a oauth token and tokensecret.</value>
        public Pair<string, string> AuthenticationPair
        {
            get
            {
                if (Authentication == null || Authentication.Authenticator == null )
                {
                    return null;
                }

                return GetAuthPairFromAuthenticator(Authentication);
            }
        }

        /// <summary>
        /// Gets the authentication pair used to authenticate to 3rd party services such as image hosts
        /// </summary>
        /// <value>The authentication pair, typically a username and password or a oauth token and tokensecret.</value>
        public Pair<string, string> SecondaryAuthenticationPair
        {
            get
            {
                if ( SecondaryAuthentication == null || SecondaryAuthentication.Authenticator == null )
                {
                    return null;
                }

                return GetAuthPairFromAuthenticator(SecondaryAuthentication);
            }
        }

        private static Pair<string, string> GetAuthPairFromAuthenticator( IFluentTwitterAuthentication authentication )
        {
            if ( authentication == null )
            {
                return null;
            }

            switch ( authentication.Mode )
            {
                case AuthenticationMode.Basic:
                    return new Pair<string, string>
                               {
                                   First = ( ( FluentTwitterBasicAuth )authentication.Authenticator ).Username,
                                   Second = ( ( FluentTwitterBasicAuth )authentication.Authenticator ).Password
                               };
                case AuthenticationMode.OAuth:
                    return new Pair<string, string>
                               {
                                   First = ( ( FluentTwitterOAuth )authentication.Authenticator ).Token,
                                   Second = ( ( FluentTwitterOAuth )authentication.Authenticator ).TokenSecret
                               };
                default:
                    throw new NotSupportedException( "Unknown authentication mode" );
            }
        }

        public bool HasAuth
        {
            get
            {
                if(Authentication == null)
                {
                    return false;
                }

                var authenticator = Authentication.Authenticator;
                if(authenticator == null)
                {
                    return false;
                }

                if(!(authenticator is FluentTwitterBasicAuth))
                {
                    return false;
                }

                return !((FluentTwitterBasicAuth) authenticator).Username.IsNullOrBlank() &&
                       !((FluentTwitterBasicAuth) authenticator).Password.IsNullOrBlank();
            }
        }

        public IFluentTwitterAuthentication Authentication { get; set; }
        public IFluentTwitterAuthentication SecondaryAuthentication { get; set; }
        
        /// <summary>
        /// Returns the key prepended to the URL when caching queries.
        /// If using basic authentication, this will be the authenticated user's username.
        /// If using OAuth, this will be the authenticated token.
        /// </summary>
        public string CacheKey
        {
            get
            {
                return AuthenticationPair != null ? AuthenticationPair.First : string.Empty;
            }
        }

        public IFluentTwitterProfile Profile { get; set; }
        public IFluentTwitterConfiguration Configuration { get; private set; }
        public IFluentTwitterSearchParameters SearchParameters { get; private set; }
        public IFluentTwitterParameters Parameters { get; private set; }

        public TwitterClientInfo ClientInfo
        {
            get
            {
                return _clientInfo;
            }
            set
            {
                _clientInfo = value;
            }
        }

        public static void SetClientInfo(TwitterClientInfo clientInfo)
        {
            _clientInfo = clientInfo;
        }
        
        public bool HasError
        {
            get
            {
                return Response != null && Response is HttpWebResponse &&
                       ((Response as HttpWebResponse).StatusCode != HttpStatusCode.OK);
            }
        }

        public TwitterRateLimitStatus RateLimitStatus
        {
            get
            {
                if(Response != null)
                {
                    var headers = Response.Headers;
                    
                    // X-RateLimit-Limit and X-RateLimit-Remaining, and X-RateLimit-Reset.
                    var limit = headers["X-RateLimit-Limit"];
                    var remaining = headers["X-RateLimit-Remaining"];
                    var reset = headers["X-RateLimit-Reset"];
                    
                    if(!(new[]{limit,remaining,reset}).AreNullOrBlank())
                    {
                        return new TwitterRateLimitStatus
                               {
                                   HourlyLimit = Convert.ToInt32(limit),
                                   RemainingHits = Convert.ToInt32(remaining),
                                   ResetTimeInSeconds = Convert.ToInt64(reset),
                                   ResetTime = Convert.ToInt64(reset).FromUnixTime()
                               };
                    }
                }

                return null;
            }
        }

        public WebResponse Response { get; private set; }
        public WebMethod Method { get; set; }
        public WebCallback Callback { get; set; }
        public WebFormat Format { get; set; }

#if !SILVERLIGHT
        public string Request()
        {
            ValidateUpdateText();

            var query = CreateWebQuery();

            // Default cache
            EnsureDefaultCache();

            // Multi-part form post for updating images
            var path = ValidatePostFilePath();
            
            switch (Method)
            {
                case WebMethod.Get:
                    return RequestGet(query);
                case WebMethod.Post:
                    return RequestPost(query, path);
                case WebMethod.Delete:
                    throw new NotImplementedException("HTTP DELETE not supported yet, use HTTP POST for now");
                default:
                    throw new NotSupportedException("Unknown or unsupported web method");
            }
        }
#endif

        private WebQueryBase CreateWebQuery()
        {
            WebQueryBase query;
            
            switch (Authentication.Mode)
            {
                case AuthenticationMode.OAuth:
                {
                    var oAuthQuery = CreateOAuthQuery();
                    query = oAuthQuery;
                    break;
                }
                case AuthenticationMode.Basic:
                {
                    if (HasAuth)
                    {
                        var authToken = AuthenticationPair.First;
                        var authSecret = AuthenticationPair.Second;

                        var basicAuthQuery = new BasicAuthWebQuery(ClientInfo, authToken, authSecret);

                        if (ClientInfo != null && !ClientInfo.ClientName.IsNullOrBlank() && Method == WebMethod.Post)
                        {
                            var postParameter = new HttpPostParameter("source", ClientInfo.ClientName);
                            basicAuthQuery.Parameters.Add(postParameter);
                        }

                        query = basicAuthQuery;
                    }
                    else
                    {
                        query = new BasicAuthWebQuery(ClientInfo);
                    }
                    break;
                }
                default:
                    throw new NotSupportedException("Only basic and OAuth authentication schemes are supported");
            }

            query.UseTransparentProxy = !string.IsNullOrEmpty(Configuration.TransparentProxy);
            query.UseCompression = Configuration.CompressHttpRequests;
            query.MockWebQueryClient = Configuration.MockWebRequests;
            query.MockGraph = Configuration.MockGraph;
            query.Method = Method;
            
            var proxy = Configuration.Proxy;
            if(!proxy.IsNullOrBlank())
            {
                if(Uri.IsWellFormedUriString(proxy, UriKind.RelativeOrAbsolute))
                {
                    query.Proxy = proxy;
                }
                else
                {
                    throw new ValidationException("A proxy '{0}' was specified but was an invalid URI".FormatWith(proxy));
                }
            }

            return query;
        }

        private OAuthWebQuery CreateOAuthQuery()
        {
            var oauth = (FluentTwitterOAuth) Authentication.Authenticator;

            var workflow = new OAuthWorkflow
                           {
                               ConsumerKey = oauth.ConsumerKey,
                               ConsumerSecret = oauth.ConsumerSecret,
                               Token = oauth.Token,
                               TokenSecret = oauth.TokenSecret,
                               SignatureMethod = OAuthSignatureMethod.HmacSha1,
                               CallbackUrl = oauth.Callback,
                               Verifier = oauth.Verifier
                           };

            var parameters = new WebParameterCollection();
            
            OAuthWebQueryInfo info;
            switch (oauth.Action)
            {
                case "resource":
                    info = workflow.BuildProtectedResourceInfo(Method, parameters, this.AsUrl());
                    break;
                case "request_token":
                    workflow.RequestTokenUrl = this.AsUrl();
                    info = workflow.BuildRequestTokenInfo(Method, parameters);
                    break;
                case "access_token":
                    workflow.AccessTokenUrl = this.AsUrl();
                    info = workflow.BuildAccessTokenInfo(Method, parameters);
                    break;
                case "authorize":
                    // convert to a token request
                    oauth.Action = "access_token";
                    workflow.AccessTokenUrl = this.AsUrl();
                    info = workflow.BuildAccessTokenInfo(Method, parameters);
                    break;
                default:
                    throw new NotSupportedException("Unknown or unsupported OAuth action");
            }

            return new OAuthWebQuery(info);
        }

        private string ValidatePostFilePath()
        {
            var hasProfileImage = !Profile.ProfileImagePath.IsNullOrBlank();
            var hasProfileBackgroundImage = !Profile.ProfileBackgroundImagePath.IsNullOrBlank();
            var isPhotoPost = !Parameters.PostImagePath.IsNullOrBlank();

            return hasProfileImage
                       ? Profile.ProfileImagePath
                       : hasProfileBackgroundImage
                             ? Profile.ProfileBackgroundImagePath
                             : isPhotoPost ? Parameters.PostImagePath : null;
        }

        private void EnsureDefaultCache()
        {
            if (Configuration.CacheStrategy == null &&
                (Configuration.CacheAbsoluteExpiration.HasValue || Configuration.CacheSlidingExpiration.HasValue))
            {
#if !Smartphone && !SILVERLIGHT
                Configuration.CacheStrategy = CacheFactory.AspNetCache;
#else
                Configuration.CacheStrategy = CacheFactory.InMemoryCache;
#endif
            }
        }

        public void RequestAsync()
        {
            ValidateUpdateText();

            var query = CreateWebQuery();

            if (Callback != null)
            {
                query.QueryResponse += query_QueryResponse;
            }

            if(RepeatInterval > TimeSpan.Zero)
            {
                // Continuous async operation
                var task = new TimedTask(TimeSpan.Zero, RepeatInterval, true, RepeatTimes,
                    () => RequestAsyncAction(query));

                TimedTasks.Add(task);
            }
            else
            {
                // Normal async operation
                RequestAsyncAction(query);    
            }
        }

        private void RequestAsyncAction(WebQueryBase query)
        {
            // Default cache
            EnsureDefaultCache();

            // Multi-part form post for updating images
            var path = ValidatePostFilePath();

            switch (Method)
            {
                case WebMethod.Get:
                    RequestGetAsync(query);
                    return;
                case WebMethod.Post:
                    RequestPostAsync(query, path);
                    return;
                case WebMethod.Delete:
                    throw new NotImplementedException("HTTP DELETE not supported yet, use HTTP POST for now");
                default:
                    throw new NotSupportedException("Unknown web method");
            }
        }

#if !SILVERLIGHT
        private string RequestPost(WebQueryBase query, string path)
        {

            var format = ValidatePhotoQuery(path);
            string result = null;

            // skip caching if we're posting multi-part form data
            if (Configuration.CacheStrategy != null && path.IsNullOrBlank())
            {
                if (Configuration.CacheAbsoluteExpiration.HasValue && Configuration.CacheSlidingExpiration.HasValue)
                {
                    throw new ArgumentException("You may only specify one cache expiration on a query");
                }

                if (Configuration.CacheAbsoluteExpiration.HasValue)
                {
                    result = query.ExecutePost(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                               Configuration.CacheAbsoluteExpiration.Value);
                }
                else
                {
                    result = Configuration.CacheSlidingExpiration.HasValue
                                 ? query.ExecutePost(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                                     Configuration.CacheSlidingExpiration.Value)
                                 : query.ExecutePost(this.AsUrl(), CacheKey, Configuration.CacheStrategy);
                }
            }

            if (result == null)
            {
                var isPhotoPost = !Parameters.PostImagePath.IsNullOrBlank() && 
                                   Parameters.PostImageProvider.HasValue;

                if(isPhotoPost)
                {
                    // external
                    SendPhotoAndAppendStatus(query, Parameters.PostImageProvider.Value, format, path);

                    // post with new media url
                    result = query.Request(this.AsUrl());
                }
                else
                {
                    if(path != null)
                    {
                        // internal
                        var file = HttpPostParameter.CreateFile("image", "twitterProfilePhoto.jpg", path, "image/jpeg");
                        result = query.Request(this.AsUrl(), new[] {file});
                    }
                    else
                    {
                        // normal flow
                        result = query.Request(this.AsUrl());
                    }
                }
            }

            Response = query.Response;
            return result.ToTwitterResponseString();
        }
#endif

        private void RequestPostAsync(WebQueryBase query, string path)
        {
#if !SILVERLIGHT
            var format = ValidatePhotoQuery(path);
#endif
            // skip caching if we're posting multi-part form data
            if (Configuration.CacheStrategy != null && path.IsNullOrBlank())
            {
                if (Configuration.CacheAbsoluteExpiration.HasValue && Configuration.CacheSlidingExpiration.HasValue)
                {
                    throw new ArgumentException("You may only specify one cache expiration on a query");
                }

                if (Configuration.CacheAbsoluteExpiration.HasValue)
                {
                    query.RequestAsync(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                           Configuration.CacheAbsoluteExpiration.Value);

                    return;
                }

                if (Configuration.CacheSlidingExpiration.HasValue)
                {
                    query.RequestAsync(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                           Configuration.CacheSlidingExpiration.Value);
                    return;
                }

                query.RequestAsync(this.AsUrl(), CacheKey, Configuration.CacheStrategy);
            }

#if !SILVERLIGHT
            var isPhotoPost = !Parameters.PostImagePath.IsNullOrBlank() && Parameters.PostImageProvider.HasValue;
            if (isPhotoPost)
            {
                // external async
                ThreadPool.QueueUserWorkItem(callback =>
                                             {
                                                 SendPhotoAndAppendStatus(query, Parameters.PostImageProvider.Value, format, path);
                                                 query.RequestAsync(this.AsUrl());
                                             });
                return;
            }

            if (path != null)
            {
                // internal async
                var file = HttpPostParameter.CreateFile("image", "twitterProfilePhoto.jpg", path, format.ToContentType());
                query.RequestAsync(this.AsUrl(), new[] {file});
                return;
            }
#endif
            // normal flow
            query.RequestAsync(this.AsUrl());
            return;
        }

#if !SILVERLIGHT
        private void SendPhotoAndAppendStatus(
            WebQueryBase query, 
            SendPhotoServiceProvider provider, 
            ImageFormat format, 
            string path)
        {
            // todo when third parties move to oauth, will have to go through that whole process for these as well
            var authPair = SecondaryAuthenticationPair ?? AuthenticationPair;
            var authToken = authPair.First;
            var authSecret = authPair.Second;
            
            var mediaUrl = SendPhotoService.SendPhoto(query, provider, format, path, authToken, authSecret);
            
            Parameters.Text = !Parameters.Text.IsNullOrBlank()
                                  ? "{0} - {1}".FormatWith(Parameters.Text, mediaUrl)
                                  : mediaUrl;
        }

        private ImageFormat ValidatePhotoQuery(string path)
        {
            var isPhotoPost = !Parameters.PostImagePath.IsNullOrBlank() && Parameters.PostImageProvider.HasValue;
            if (!path.IsNullOrBlank())
            {
                switch(isPhotoPost)
                {
                    case true:
                        // external
                        return ValidateProviderPostImage(path, Parameters.PostImageProvider.Value);
                    case false:
                        // internal
                        ValidateTwitterPostImage(path);
                        break;
                    default:
                        throw new NotSupportedException("No fuzzy logic allowed");
                }
            }

            return null;
        }

        private static ImageFormat ValidateProviderPostImage(string path, SendPhotoServiceProvider provider)
        {
            switch(provider)
            {
                // todo refactor to generic size / formats validation
                case SendPhotoServiceProvider.TwitPic:
                    return ValidateProviderPostImage(path, 4.Megabytes());   
                case SendPhotoServiceProvider.YFrog:
                    // todo unclear on YFrog size limitations
                    return ValidateProviderPostImage(path, 4.Megabytes());
                case SendPhotoServiceProvider.TwitGoo:
                    // todo unclear on TwitGoo size limitations
                    return ValidateProviderPostImage(path, 16.Megabytes());
                default:
                    throw new NotSupportedException("Not a recognized photo posting service");
            }
        }

        private static ImageFormat ValidateProviderPostImage(string path, long size)
        {
            try
            {
                // valid image
                var info = new FileInfo(path);
                if (info.Length > size)
                {
                    throw new ValidationException(string.Format("The image provided was larger than 4Mb, actual size was {0}", info.Length));
                }
#if !Smartphone
                var image = Image.FromFile(path);
                return image.RawFormat;
#else
                return ImageFormat.Bmp;
#endif
            }
            catch (IOException iex)
            {
                throw new ValidationException("The path to the image provided was invalid", iex);
            }
            catch (Exception ex)
            {
                throw new ValidationException("Unable to process the provided path as an image", ex);
            }
        }

        private static void ValidateTwitterPostImage(string path)
        {
            try
            {
#if !Smartphone
                var image = Image.FromFile(path);
                if (ImageFormat.Jpeg.Equals(image.RawFormat))
                {
                    // valid
                }
                else if (ImageFormat.Gif.Equals(image.RawFormat))
                {
                    // valid
                }
                else if (ImageFormat.Png.Equals(image.RawFormat))
                {
                    // valid
                }
                else
                {
                    throw new ValidationException("The image provided was not a supported type");
                }
#endif

                var info = new FileInfo(path);
                if (info.Length > 700.Kilobytes())
                {
                    throw new ValidationException(
                        string.Format("The image provided was larger than 700kb, actual size was {0}", info.Length));
                }

                return;
            }
            catch (IOException iex)
            {
                throw new ValidationException("The path to the image provided was invalid", iex);
            }
            catch (Exception ex)
            {
                throw new ValidationException("Unable to process the provided path as an image", ex);
            }
        }
#endif

#if !SILVERLIGHT
        private string RequestGet(WebQueryBase query)
        {
            string result = null;

            if (Configuration.CacheStrategy != null)
            {
                if (Configuration.CacheAbsoluteExpiration.HasValue && Configuration.CacheSlidingExpiration.HasValue)
                {
                    throw new ArgumentException("You may only specify one cache expiration on a query");
                }

                if (Configuration.CacheAbsoluteExpiration.HasValue)
                {
                    result = query.Request(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                              Configuration.CacheAbsoluteExpiration.Value);
                }
                else
                {
                    result = Configuration.CacheSlidingExpiration.HasValue
                                 ? query.Request(this.AsUrl(), CacheKey, Configuration.CacheStrategy,
                                                    Configuration.CacheSlidingExpiration.Value)
                                 : query.Request(this.AsUrl(), CacheKey, Configuration.CacheStrategy);
                }
            }

            if (result == null)
            {
                var url = this.AsUrl();
                result = query.Request(url);
            }

            Response = query.Response;
            return result.ToTwitterResponseString();
        }
#endif

        private void RequestGetAsync(WebQueryBase query)
        {
            var url = this.AsUrl();

            if (Configuration.CacheStrategy != null)
            {
                if (Configuration.CacheAbsoluteExpiration.HasValue && Configuration.CacheSlidingExpiration.HasValue)
                {
                    throw new ArgumentException("You may only specify one cache expiration on a query");
                }

                if (Configuration.CacheAbsoluteExpiration.HasValue)
                {
                    query.RequestAsync(url, CacheKey, Configuration.CacheStrategy,
                                  Configuration.CacheAbsoluteExpiration.Value);

                    return;
                }

                if(Configuration.CacheSlidingExpiration.HasValue)
                {
                    query.RequestAsync(url, CacheKey, Configuration.CacheStrategy,
                                       Configuration.CacheSlidingExpiration.Value);
                    return;
                }

                query.RequestAsync(url, CacheKey, Configuration.CacheStrategy);
            }

            query.RequestAsync(url);
        }

        /// <summary>
        /// Creates a new composable query, using a specified client and a default platform.
        /// </summary>
        /// <param name="clientInfo">The client making the request</param>
        public static IFluentTwitter CreateRequest(TwitterClientInfo clientInfo)
        {
            return new FluentTwitter(clientInfo);
        }

        /// <summary>
        /// Creates a new composable query, using the default client and platform.
        /// </summary>
        public static IFluentTwitter CreateRequest()
        {
            if (_clientInfo == null)
            {
                _clientInfo = new TwitterClientInfo
                                  {
                                      ClientName = TWITTER_CLIENT_DEFAULT_NAME,
                                      ClientUrl = TWITTER_CLIENT_DEFAULT_URL,
                                      ClientVersion = TWITTER_CLIENT_DEFAULT_VERSION
                                  };
            }
            
            return new FluentTwitter(_clientInfo);
        }

        public void ValidateUpdateText()
        {
            if (Parameters.Text == null)
            {
                // non-participant
                return;
            }

            if (Parameters.Text.IsNullOrBlank())
            {
                if(!Parameters.PostImagePath.IsNullOrBlank() &&
                    Parameters.PostImageProvider.HasValue)
                {
                    // photo statuses have no text initially
                    return;
                }

                throw new ArgumentException("Status text must contain at least one character");
            }

            var words = Parameters.Text.Split(' ').ToList();

#if !SILVERLIGHT
            ShortenUrls(words);
#endif

            if (Parameters.Text.Length <= TWITTER_MAX_UPDATE_LENGTH)
            {
                // valid
                return;
            }

            switch (Configuration.TruncateUpdates)
            {
                case true:
                    while (Parameters.Text.Length > TWITTER_MAX_UPDATE_LENGTH)
                    {
                        if (words.Count > 1)
                        {
                            var last = words.Last();
                            Parameters.Text = Parameters.Text.RemoveRange(Parameters.Text.LastIndexOf(last),
                                                                          Parameters.Text.Length);
                            words.Remove(last);
                        }
                        else
                        {
                            if (Parameters.Text.Length == 1)
                            {
                                throw new ValidationException("This shouldn't have happened");
                            }

                            Parameters.Text = Parameters.Text.Substring(0, Parameters.Text.Length - 1);
                        }
                    }

                    ValidateUpdateText();
                    break;
                default:
                    throw new ValidationException(
                        "Status length of {0} exceeds the maximum length of {1}".FormatWith(Parameters.Text.Length,
                                                                                            TWITTER_MAX_UPDATE_LENGTH));
            }
        }

#if !SILVERLIGHT
        private void ShortenUrls(IEnumerable<string> words)
        {
            if (!Configuration.ShortenUrls)
            {
                return;
            }

            if(Configuration.ShortenUrlService == null)
            {
                return;
            }

            var provider = Configuration.ShortenUrlService.Value;
            var username = Configuration.ShortenUrlUsername;
            var password = Configuration.ShortenUrlPassword;
            var apiKey = Configuration.ShortenUrlApiKey;

            Parameters.Text = ShortenUrlService.ShortenUrl(provider, words, Parameters.Text, username, password, apiKey);
        }
#endif

        private void query_QueryResponse(object sender, WebQueryResponseEventArgs e)
        {
            if (Callback == null)
            {
                return;
            }

            var query = sender as WebQueryBase;
            if(query != null)
            {
                Response = query.Response;
            }

            e.Response = e.Response.ToTwitterResponseString();
            Callback(this, e);
        }

        /// <summary>
        /// Returns the human-readable query to Twitter representing the current expression.
        /// If you are storing URLs for sending later, you can use <code>AsUrl()</code> to return
        /// a URL-encoded string instead.
        /// </summary>
        /// <returns>A URL-decoded string representing this expression's query to Twitter</returns>
        public override string ToString()
        {
            // human-readable; for storing urls, use AsUrl()
            return this.AsUrl().UrlDecode();
        }
    }
}